Go와 OpenTelemetry를 사용한 포괄적인 마이크로서비스 관찰 가능성 달성
Wenhao Wang
Dev Intern · Leapcell

소개
현대 소프트웨어 개발의 빠르게 진화하는 환경에서 마이크로서비스는 확장 가능하고 복원력이 있으며 독립적으로 배포 가능한 애플리케이션을 구축하기 위한 사실상의 아키텍처 스타일이 되었습니다. 마이크로서비스는 민첩성과 유지 관리성 측면에서 부인할 수 없는 이점을 제공하지만, 상당한 복잡성을 야기합니다. 특히 분산 시스템에서 요청이 어떻게 흐르는지 이해하는 데 어려움이 있습니다. 단일 사용자 상호 작용은 수많은 서비스를 가로지르는 호출의 연쇄를 트리거할 수 있으며, 적절한 가시성 없이는 성능 병목 현상을 진단하거나, 오류를 정확히 찾아내거나, 시스템의 전반적인 상태를 파악하는 것이 엄청나게 어렵습니다.
이것이 분산 추적의 개념이 빛을 발하는 지점입니다. 분산 추적을 통해 관련된 모든 서비스에서 요청의 전체 여정을 시각화할 수 있으며, 지연, 오류 및 서비스 간 종속성에 대한 귀중한 통찰력을 제공합니다. 고성능 마이크로서비스 구축에 Go가 중요해짐에 따라 강력한 추적 솔루션을 통합하는 것이 가장 중요합니다. 산업 표준 오픈 소스 관찰 가능성 프레임워크인 OpenTelemetry는 추적, 메트릭 및 로그 수집을 위한 통합된 접근 방식을 제공합니다. 이 문서는 Go 마이크로서비스에 OpenTelemetry를 통합하는 과정을 안내하여 포괄적인 전체 스택 추적을 활성화하고 분산 애플리케이션에 대한 탁월한 가시성을 제공합니다.
분산 추적의 핵심 개념 이해
구현에 뛰어들기 전에 분산 추적 및 OpenTelemetry의 핵심 개념에 대한 공통된 이해를 확립해 보겠습니다.
- 추적(Trace): 추적은 분산 시스템을 통해 전파되는 단일 요청 또는 트랜잭션의 전체 실행 경로를 나타냅니다. 이는 순서가 지정된 스팬들의 모음입니다.
- 스팬(Span): 스팬은 추적 내에서 논리적 작업 단위를 나타내는 이름이 지정된 시간 기반 작업입니다. 각 스팬에는 시작 및 종료 시간, 이름 및 속성이 있습니다. 스팬은 중첩되어 부모-자식 관계를 형성할 수 있습니다. 예를 들어, API 요청은 최상위 스팬을 생성할 수 있으며, 이는 데이터베이스 호출, 외부 서비스 호출 또는 내부 비즈니스 로직 실행에 대한 자식 스팬을 가집니다.
- 컨텍스트 전파(Context Propagation): 요청이 시스템을 통해 이동할 때 추적 정보(예:
trace_id
및span_id
)가 서비스 간에 전달되는 메커니즘입니다. 이는 스팬을 연결하여 완전한 추적을 형성하는 데 중요합니다. OpenTelemetry는 상호 운용성을 보장하기 위해 합의된 컨텍스트 형식(예: W3C Trace Context)을 사용합니다. - 추적기 제공자(Tracer Provider):
Tracer
인스턴스를 만드는 진입점입니다. 내보내기, 샘플링기 및 리소스 속성을 구성합니다. - 추적기(Tracer):
Span
객체를 만드는 데 사용되는 인터페이스입니다. - 내보내기(Exporter): 완료된 스팬을 백엔드 시스템(예: Jaeger, Zipkin, OTLP 수집기)으로 보내 저장 및 분석하는 역할을 합니다.
- 샘플링기(Sampler): 어떤 추적을 기록하고 내보낼지 결정합니다. 샘플링은 특히 처리량이 높은 시스템에서 추적 데이터 볼륨을 제어하는 데 사용될 수 있습니다.
Go와 OpenTelemetry를 사용한 전체 스택 추적 구현
간단한 Go 마이크로서비스 아키텍처에 OpenTelemetry를 통합하는 방법을 설명하겠습니다. Order Service
와 Product Service
의 두 가지 서비스를 고려할 것입니다. Order Service
는 주문 생성을 처리하고 Product Service
는 제품 세부 정보를 검색합니다.Order Service
는 Product Service
를 호출합니다.
먼저 두 서비스 모두에 OpenTelemetry를 설정해야 합니다.
1. Go에서 OpenTelemetry 초기화
Jaeger 내보내기를 사용하여 OpenTelemetry를 초기화하는 유틸리티 함수를 만들 것입니다. Jaeger는 널리 사용되는 오픈 소스 분산 추적 시스템입니다.
// common/otel.go package common import ( "context" "fmt" "log" "os" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/jaeger" "go.opentelemetry.io/otel/sdk/resource" tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.7.0" ) // InitTracerProvider는 OpenTelemetry TracerProvider를 초기화합니다. func InitTracerProvider(serviceName string) (func(context.Context) error, error) { // Jaeger 내보내기 생성 url := "http://localhost:14268/api/traces" // 기본 Jaeger 수집기 엔드포인트 exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url))) if err != nil { return nil, fmt.Errorf("Jaeger 내보내기 생성 실패: %w", err) } // Jaeger 내보내기와 BatchSpanProcessor를 사용하여 스팬을 효율적으로 보냅니다. tp := tracesdk.NewTracerProvider( tracesdk.WithBatchProcessor(tracesdk.NewBatchSpanProcessor(exporter)), // 리소스는 서비스와 해당 속성을 식별합니다. tracesdk.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(serviceName), attribute.String("environment", "development"), )), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(otel.NewCompositeTextMapPropagator( // W3C Trace Context 및 B3(하위 호환성을 위해 필요한 경우)에 대한 표준 컨텍스트 전파기. // 주로 W3C Trace Context를 사용합니다. otel.GetTextMapPropagator(), // 기본값은 W3C Trace Context를 포함합니다. )) log.Printf("OpenTelemetry가 서비스에 대해 초기화되었습니다: %s", serviceName) return tp.Shutdown, nil }
이 InitTracerProvider
함수는 다음을 수행합니다.
- Jaeger 내보내기 구성: OpenTelemetry가 로컬에서 실행되는 Jaeger 수집기로 추적을 보내도록 지시합니다.
TracerProvider
생성: 이 제공자는Tracer
인스턴스를 관리하고 스팬이 처리되는 방식을 구성합니다(예: 효율성을 위해BatchSpanProcessor
사용).Resource
속성 설정: 이러한 속성은 서비스 자체에 대한 메타데이터(예: 서비스 이름, 환경)를 제공합니다.TextMapPropagator
설정: 이는 컨텍스트 전파에 중요합니다. 추적 컨텍스트를 요청 헤더로 삽입하고 추출하는 방법을 구성합니다.otel.GetTextMapPropagator()
는 기본적으로W3C Trace Context
를 포함하며, 이는 권장되는 표준입니다.
2. Product Service 구현
Product Service
는 단순히 제품 목록을 반환합니다. 수신 HTTP 요청에 대한 스팬을 자동으로 생성하도록 계측할 것입니다.
// product-service/main.go package main import ( "context" "fmt" "log" "net/http" "os" "time" "github.com/yourusername/app/common" // common/otel.go가 여기에 있다고 가정 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) func productsHandler(w http.ResponseWriter, r *http.Request) { // otelhttp에서 자동으로 생성된 요청 컨텍스트에서 스팬을 가져옵니다. // 이 스팬에 사용자 지정 속성을 추가하거나 자식 스팬을 만들 수 있습니다. span := trace.SpanFromContext(r.Context()) span.SetAttributes(attribute.String("product.category", "electronics")) // 일부 작업 시뮬레이션 time.Sleep(50 * time.Millisecond) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"products": [{"id": "prod1", "name": "Laptop"}, {"id": "prod2", "name": "Monitor"}]}`)) log.Println("Responded to /products request") } func main() { // OpenTelemetry 초기화 shutdown, err := common.InitTracerProvider("product-service") if err != nil { log.Fatalf("OpenTelemetry 초기화 실패: %v", err) } defer func() { if err := shutdown(context.Background()); err != nil { log.Fatalf("TracerProvider 종료 실패: %v", err) } }() // otelhttp.NewHandler를 사용하여 HTTP 서버 계측 http.Handle("/products", otelhttp.NewHandler(http.HandlerFunc(productsHandler), "/products")) port := ":8081" log.Printf("Product Service가 %s에서 수신 대기 중입니다.", port) if err := http.ListenAndServe(port, nil); err != nil { log.Fatalf("Product Service 시작 실패: %v", err) } }
Product Service
의 주요 사항:
common.InitTracerProvider
: OpenTelemetry를 초기화합니다.otelhttp.NewHandler
:go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
의 편의 래퍼입니다. 각 요청에 대한 스팬을 자동으로 가로채고 생성하며(헤더에 부모 컨텍스트가 있는 경우 추출), 해당 스팬을 사용하여 계측된 컨텍스트를 사용하도록 서버의 HTTP 핸들러를 설정합니다.trace.SpanFromContext(r.Context())
: 요청 컨텍스트에서 현재 스팬을 검색하고 사용자 지정 속성을 추가하여 작업에 대한 더 세분화된 세부 정보를 제공할 수 있습니다.
3. Order Service 구현
Order Service
는 주문을 생성하는 엔드포인트를 노출합니다. 이 엔드포인트는 차례로 제품 세부 정보를 가져오기 위해 Product Service
에 HTTP 호출을 합니다.
// order-service/main.go package main import ( "context" "fmt" "io" "log" "net/http" "os" "time" "github.com/yourusername/app/common" // common/otel.go가 여기에 있다고 가정 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) var tracer = otel.Tracer("order-service") func createOrderHandler(w http.ResponseWriter, r *http.Request) { // 전체 주문 생성 프로세스를 위한 새 스팬을 만듭니다. // 부모 컨텍스트는 수신 HTTP 요청(otelhttp.NewHandler를 통해)에서 암묵적으로 가져옵니다. ctx, span := tracer.Start(r.Context(), "createOrder") defer span.End() span.SetAttributes(attribute.String("order.id", "order123")) log.Println("Order Service: 주문 생성 요청 수신") // 초기 처리 시뮬레이션 time.Sleep(10 * time.Millisecond) // Product Service에 HTTP 호출 수행 productSvcURL := "http://localhost:8081/products" req, err := http.NewRequestWithContext(ctx, "GET", productSvcURL, nil) if err != nil { span.RecordError(err) span.SetAttributes(attribute.Bool("error", true)) http.Error(w, fmt.Sprintf("요청 생성 실패: %v", err), http.StatusInternalServerError) return } // HTTP 클라이언트 호출 계측 // otelhttp.Client는 추적 컨텍스트를 다운스트림 서비스로 전파하는 데 중요합니다. client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} log.Printf("Order Service: Product Service 호출 중 %s", productSvcURL) resp, err := client.Do(req) if err != nil { span.RecordError(err) span.SetAttributes(attribute.Bool("error", true)) http.Error(w, fmt.Sprintf("Product Service 호출 실패: %v", err), http.StatusInternalServerError) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { span.SetAttributes(attribute.Bool("error", true)) http.Error(w, fmt.Sprintf("Product Service가 non-200 상태 반환: %d", resp.StatusCode), http.StatusInternalServerError) return } body, err := io.ReadAll(resp.Body) if err != nil { span.RecordError(err) span.SetAttributes(attribute.Bool("error", true)) http.Error(w, fmt.Sprintf("Product Service 응답 읽기 실패: %v", err), http.StatusInternalServerError) return } log.Printf("Order Service: 제품 수신: %s", string(body)) // 최종 주문 저장 시뮬레이션 time.Sleep(20 * time.Millisecond) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) w.Write([]byte(fmt.Sprintf(`{"message": "Order created successfully with products: %s"}`, string(body)))) log.Println("Order Service: 주문이 성공적으로 생성되었습니다.") } func main() { // OpenTelemetry 초기화 shutdown, err := common.InitTracerProvider("order-service") if err != nil { log.Fatalf("OpenTelemetry 초기화 실패: %v", err) } defer func() { if err := shutdown(context.Background()); err != nil { log.Fatalf("TracerProvider 종료 실패: %v", err) } }() // Order Service에 대한 수신 HTTP 서버 계측 http.Handle("/order", otelhttp.NewHandler(http.HandlerFunc(createOrderHandler), "/order")) port := ":8080" log.Printf("Order Service가 %s에서 수신 대기 중입니다.", port) if err := http.ListenAndServe(port, nil); err != nil { log.Fatalf("Order Service 시작 실패: %v", err) } }
Order Service
의 주요 사항:
tracer.Start(r.Context(), "createOrder")
:createOrder
작업에 대한 새 스팬을 수동으로 만듭니다. 중요한 것은r.Context()
를 전달하는 것인데, 여기에는 수신 요청에 대한otelhttp.NewHandler
에서 전파된 추적 컨텍스트가 포함되어createOrder
가 수신 요청 스팬의 자식 스팬이 되도록 합니다.http.NewRequestWithContext(ctx, "GET", productSvcURL, nil)
: 나가는 요청을 할 때 현재 추적context.Context
(tracer.Start
의ctx
)를 전달하는 것이 중요합니다. 이렇게 하면 추적 ID와 부모 스팬 ID가 요청 컨텍스트에 포함됩니다.client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
:otelhttp.NewTransport
는 기본 HTTP 클라이언트 전송을 래핑하는 데 사용됩니다. 이 래퍼는 추적 컨텍스트를req.Context()
에서 나가는 HTTP 요청 헤더(예:traceparent
헤더)로 자동으로 삽입합니다. 이것이 서비스 간 컨텍스트 전파를 활성화하는 핵심입니다.span.RecordError(err)
및span.SetAttributes(attribute.Bool("error", true))
: 오류가 발생했을 때 오류를 기록하고 스팬을 오류로 표시하는 것이 모범 사례입니다. 이렇게 하면 다시 실행하기에 문제가 있는 추적을 쉽게 필터링할 수 있습니다.
예제 실행
-
Jaeger 시작: Docker를 사용하여 Jaeger를 실행할 수 있습니다.
docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -p 6831:6831/udp -p 16686:16686 jaegertracing/all-in-one:latest
그런 다음 Jaeger UI에
http://localhost:16686
에서 액세스합니다. -
서비스 빌드 및 실행:
# product-service 디렉토리에서 go mod init github.com/yourusername/app/product-service go mod tidy go run main.go # order-service 디렉토리에서 go mod init github.com/yourusername/app/order-service go mod tidy go run main.go
common/otel.go
가 접근 가능하도록 하십시오. 예를 들어product-service
및order-service
와 동일한 수준의common
디렉토리에 배치하고 가져오기 경로를 조정합니다. -
요청 보내기:
curl http://localhost:8080/order
-
Jaeger UI에서 관찰:
http://localhost:16686
으로 이동하여 서비스를order-service
로 선택하고 추적을 찾습니다. 수신 요청,createOrder
스팬, 나가는 HTTP 클라이언트 호출에 대한order-service
스팬과product-service
의 수신 요청을 나타내는 자식 스팬이 있는 추적을 볼 수 있습니다.
이점 및 적용 시나리오
전체 스택 추적을 위해 OpenTelemetry를 통합하면 수많은 이점을 얻을 수 있습니다.
- 더 빠른 문제 해결: 요청 흐름을 시각화하여 지연 또는 오류를 유발하는 정확한 서비스 또는 구성 요소를 신속하게 파악합니다.
- 성능 모니터링: 느린 데이터베이스 쿼리, 비효율적인 API 호출 또는 높은 지연 시간 외부 종속성과 같이 마이크로서비스 간의 성능 병목 현상을 식별합니다.
- 근본 원인 분석: 어떤 서비스가 관련되어 있고 해당 상태는 무엇인지 포함하여 오류의 컨텍스트를 추적하여 효과적인 근본 원인 식별을 지원합니다.
- 서비스 종속성 매핑: 복잡한 아키텍처를 이해하는 데 귀중한 마이크로서비스 간의 종속성을 자동으로 검색하고 시각화합니다.
- 향상된 관찰 가능성: 포괄적인 관찰 가능성 전략으로 나아가는 원격 분석 데이터(추적, 메트릭 및 로그) 수집 및 내보내기에 대한 일관되고 통일된 접근 방식을 제공합니다.
- 공급업체 중립성: OpenTelemetry는 개방형 표준이므로 애플리케이션 코드를 변경하지 않고도 다른 관찰 가능성 백엔드(Jaeger, Zipkin, DataDog, New Relic 등) 간에 전환할 수 있습니다.
이 설정은 전자 상거래 플랫폼에서 금융 서비스에 이르기까지 실시간 시스템 동작을 이해하는 것이 중요한 모든 Go 마이크로서비스 애플리케이션에 중요합니다.
결론
마이크로서비스 세계에서 전체 스택 추적은 분산 애플리케이션의 복잡한 춤에 대한 깊은 가시성을 제공하는 필수 도구입니다. Go와 OpenTelemetry를 활용하면 개발자는 표준화된 공급업체 독립적 프레임워크를 사용하여 서비스를 계측할 수 있습니다. 이 통합은 불투명한 분산 시스템을 투명하고 관찰 가능한 개체로 변환하여 문제 해결, 성능 최적화 및 전반적인 시스템 이해를 크게 단순화합니다. OpenTelemetry를 채택하는 것은 진정으로 강력하고 유지 관리 가능한 마이크로서비스 아키텍처로 향하는 길을 열어줍니다.